agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH v25 2/4] Refactor index_concurrently_create_copy() for use with REPACK (CONCURRENTLY). 1737+ messages / 2 participants [nested] [flat]
* [PATCH v25 2/4] Refactor index_concurrently_create_copy() for use with REPACK (CONCURRENTLY). @ 2025-08-11 13:31 Antonin Houska <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Antonin Houska @ 2025-08-11 13:31 UTC (permalink / raw) This patch moves the code to index_create_copy() and adds a "concurrently" parameter so it can be used by REPACK (CONCURRENTLY). With the CONCURRENTLY option, REPACK cannot simply swap the heap file and rebuild its indexes. Instead, it needs to build a separate set of indexes (including system catalog entries) *before* the actual swap, to reduce the time AccessExclusiveLock needs to be held for. --- src/backend/catalog/index.c | 36 ++++++++++++++++++++++++++++-------- src/include/catalog/index.h | 3 +++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 08d4b8e44d7..2b33bf04883 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1290,15 +1290,31 @@ index_create(Relation heapRelation, /* * index_concurrently_create_copy * - * Create concurrently an index based on the definition of the one provided by - * caller. The index is inserted into catalogs and needs to be built later - * on. This is called during concurrent reindex processing. - * - * "tablespaceOid" is the tablespace to use for this index. + * Variant of index_create_copy(), called during concurrent reindex + * processing. */ Oid index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, Oid tablespaceOid, const char *newName) +{ + return index_create_copy(heapRelation, oldIndexId, tablespaceOid, newName, + true); +} + +/* + * index_create_copy + * + * Create an index based on the definition of the one provided by caller. The + * index is inserted into catalogs and needs to be built later on. + * + * "tablespaceOid" is the tablespace to use for this index. + * + * The actual implementation of index_concurrently_create_copy(), reusable for + * other purposes. + */ +Oid +index_create_copy(Relation heapRelation, Oid oldIndexId, Oid tablespaceOid, + const char *newName, bool concurrently) { Relation indexRelation; IndexInfo *oldInfo, @@ -1317,6 +1333,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, List *indexColNames = NIL; List *indexExprs = NIL; List *indexPreds = NIL; + int flags = 0; indexRelation = index_open(oldIndexId, RowExclusiveLock); @@ -1325,9 +1342,9 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, /* * Concurrent build of an index with exclusion constraints is not - * supported. + * supported. If !concurrently, ii_ExclusinOps is currently not needed. */ - if (oldInfo->ii_ExclusionOps != NULL) + if (oldInfo->ii_ExclusionOps != NULL && concurrently) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("concurrent index creation for exclusion constraints is not supported"))); @@ -1435,6 +1452,9 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, stattargets[i].isnull = isnull; } + if (concurrently) + flags = INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT; + /* * Now create the new index. * @@ -1458,7 +1478,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indcoloptions->values, stattargets, reloptionsDatum, - INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT, + flags, 0, true, /* allow table to be a system catalog? */ false, /* is_internal? */ diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index dda95e54903..4bf909078d8 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -99,6 +99,9 @@ extern Oid index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, Oid tablespaceOid, const char *newName); +extern Oid index_create_copy(Relation heapRelation, Oid oldIndexId, + Oid tablespaceOid, const char *newName, + bool concurrently); extern void index_concurrently_build(Oid heapRelationId, Oid indexRelationId); -- 2.47.3 --ecl2euvoqutt27wj Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v25-0003-Move-conversion-of-a-historic-to-MVCC-snapshot-t.patch" ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0c..3c3b35910e1 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4092,6 +4095,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f1dd55213d..0a8a2772b23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -20,6 +20,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --zB/Wek73NRZcta1g-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions @ 2026-04-27 14:23 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 1737+ messages in thread From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw) Add instrumentation under USE_ASSERT_CHECKING to detect cases where object_aclcheck() is called on a referenced object before a lock is held on it, which would widen the TOCTOU window between the permission check and the dependency recording. In recordMultipleDependencies() and changeDependencyFor(), for each referenced object that had a permission check earlier in the same statement, assert that a lock is already held. This catches any future code that adds a permission check on a referenced object without first acquiring a lock. The tracking records each (classId, objectId, mode) from object_aclcheck() and pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE, ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking resets at each ProcessUtility() call. The tracking array is capped at 1024 entries per statement, which is sufficient for any practical DDL command. Author: Bertrand Drouvot <[email protected]> Reviewed-by: Discussion: https://postgr.es/m/[email protected] --- src/backend/catalog/aclchk.c | 32 ++++++++++++ src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++ src/backend/tcop/utility.c | 3 ++ src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 5 files changed, 161 insertions(+) 51.5% src/backend/catalog/ 46.7% src/include/catalog/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 007ede997c5..d13667f38a8 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -76,6 +76,7 @@ #include "parser/parse_type.h" #include "storage/lmgr.h" #include "utils/acl.h" +#include "catalog/aclcheck_track.h" #include "utils/aclchk_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(classid, objectid, mode); + if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -4093,6 +4096,8 @@ AclResult pg_class_aclcheck_ext(Oid table_oid, Oid roleid, AclMode mode, bool *is_missing) { + aclcheck_track_record(RelationRelationId, table_oid, mode); + if (pg_class_aclmask_ext(table_oid, roleid, mode, ACLMASK_ANY, is_missing) != 0) return ACLCHECK_OK; @@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) table_close(rel, RowExclusiveLock); } + +/* + * Instrumentation to detect permission check before lock regressions. + * Only used in assert-enabled builds. + */ +#ifdef USE_ASSERT_CHECKING +AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX]; +int aclcheck_tracked_count = 0; + +void +aclcheck_track_reset(void) +{ + aclcheck_tracked_count = 0; +} + +bool +aclcheck_track_was_checked(Oid classId, Oid objectId) +{ + for (int i = 0; i < aclcheck_tracked_count; i++) + { + if (aclcheck_tracked[i].classId == classId && + aclcheck_tracked[i].objectId == objectId) + return true; + } + return false; +} +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index 5618e3d26fa..28fa99b2d42 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" +#include "catalog/aclcheck_track.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/pg_constraint.h" @@ -27,6 +28,8 @@ #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender, if (isObjectPinned(referenced)) continue; +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not + * after. The latter would widen the TOCTOU window between the + * permission check and the dependency recording. + */ + if (aclcheck_track_was_checked(referenced->classId, referenced->objectId)) + { + if (referenced->classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + referenced->classId, + referenced->objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif + /* * Acquire a lock and check object still exists while recording the * dependency. @@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId, return 1; } +#ifdef USE_ASSERT_CHECKING + /* + * If this referenced object had a permission check earlier in this + * statement, assert that a lock is already held on it. This ensures + * callers acquired the lock before calling object_aclcheck(), not after. + * The latter would widen the TOCTOU window between the permission check and + * the dependency recording. + */ + if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId)) + { + if (objAddr.classId == RelationRelationId) + Assert(CheckRelationOidLockedByMe(objAddr.objectId, + AccessShareLock, true)); + else + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, MyDatabaseId, + objAddr.classId, + objAddr.objectId, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); + } + } +#endif /* * Acquire a lock and check object still exists while changing the * dependency. diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 73a56f1df1d..0c4d382bd37 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -21,6 +21,7 @@ #include "access/xact.h" #include "access/xlog.h" #include "catalog/namespace.h" +#include "catalog/aclcheck_track.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" #include "catalog/toasting.h" @@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt, Assert(queryString != NULL); /* required as of 8.4 */ Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN); + aclcheck_track_reset(); + /* * We provide a function hook variable that lets loadable plugins get * control when ProcessUtility is called. Such a plugin would normally diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h new file mode 100644 index 00000000000..5825d7398d3 --- /dev/null +++ b/src/include/catalog/aclcheck_track.h @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------- + * + * aclcheck_track.h + * Instrumentation to detect permission check before lock via Assert in + * recordMultipleDependencies() and changeDependencyFor(). + * + * Only active in USE_ASSERT_CHECKING builds. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/aclcheck_track.h + * + *------------------------------------------------------------------------- + */ +#ifndef ACLCHECK_TRACK_H +#define ACLCHECK_TRACK_H + +#ifdef USE_ASSERT_CHECKING + +#include "catalog/pg_namespace.h" + +#define ACLCHECK_TRACK_MAX 1024 + +typedef struct AclCheckEntry +{ + Oid classId; + Oid objectId; +} AclCheckEntry; + +extern AclCheckEntry aclcheck_tracked[]; +extern int aclcheck_tracked_count; + +extern void aclcheck_track_reset(void); +extern bool aclcheck_track_was_checked(Oid classId, Oid objectId); + +/* + * Only record aclchecks that are dependency-relevant: + * - ACL_CREATE on any object (creating something in a container) + * - ACL_USAGE on non-namespace objects (using a type, language, server) + * - ACL_EXECUTE on functions + * - ACL_TRIGGER, ACL_REFERENCES on relations + * + * Skip ACL_USAGE on namespaces — that's name resolution, not dependency. + */ +static inline void +aclcheck_track_record(Oid classId, Oid objectId, AclMode mode) +{ + /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */ + if (classId == NamespaceRelationId && !(mode & ACL_CREATE)) + return; + + /* Only track dependency-relevant modes */ + if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES))) + return; + + if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX) + { + aclcheck_tracked[aclcheck_tracked_count].classId = classId; + aclcheck_tracked[aclcheck_tracked_count].objectId = objectId; + aclcheck_tracked_count++; + } +} + +#else /* !USE_ASSERT_CHECKING */ + +#define aclcheck_track_reset() ((void) 0) +#define aclcheck_track_record(c, o, m) ((void) 0) +#define aclcheck_track_was_checked(c, o) (false) + +#endif /* USE_ASSERT_CHECKING */ + +#endif /* ACLCHECK_TRACK_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8cf40c87043..cb0cc25bda8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -19,6 +19,7 @@ AbsoluteTime AccessMethodInfo AccessPriv Acl +AclCheckEntry AclItem AclMaskHow AclMode -- 2.34.1 --FjYZKkuBdH+F20sI-- ^ permalink raw reply [nested|flat] 1737+ messages in thread
end of thread, other threads:[~2026-04-27 14:23 UTC | newest] Thread overview: 1737+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2025-08-11 13:31 [PATCH v25 2/4] Refactor index_concurrently_create_copy() for use with REPACK (CONCURRENTLY). Antonin Houska <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]> 2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox